package com.coder.rental.security;

import cn.hutool.core.util.StrUtil;
import com.coder.rental.utils.JwtUtils;
import com.coder.rental.utils.RedisUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;


import java.io.IOException;

/**
 * @author: zhanglin
 * @description:  OncePerRequestFilter是Springboot提供的过滤器抽象类，
 * 在SpringSecurity中用来过滤请求，用于继承实现并在每次请求时执行
 * @date 2025/3/28
 */
@Component
public class VerifyTokenFilter extends OncePerRequestFilter {
    //获取登录地址
    @Value("${request.login-url}")
    private String loginUrl;

    @Resource
    private RedisUtils redisUtils;

    @Resource
    private CustomerUserDetailsService customerUserDetailsService;

    @Resource
    private LoginFailHandler loginFailHandler;
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        //获取当前请求的url地址
        String url = request.getRequestURI();
        //当请求的URL不是登录页面的URl时，进行令牌校验
        if(!StrUtil.equals(url, loginUrl)){
            try{
                validateToken(request,response);//验证令牌
            }catch (AuthenticationException e){
                //令牌验证失败，调用登录失败处理器处理异常
                loginFailHandler.onAuthenticationFailure(request, response, e);
            }
        }
        //校验成功继续过滤器链的处理操作
        doFilter(request, response, filterChain);
    }
    /*
    * 验证令牌token的有效性
    * request对象，用于获取请求头或参数中的token
    * response对象，用于验证失败时设置响应状态码
    * 
    * */
    private void validateToken(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException{
        //01-从请求头中获取token，如果不存在则从请求参数中获取
        String token = request.getHeader("token");
        if(StrUtil.isEmpty(token)){
            token = request.getParameter("token");
        }
        //02-如果token为空，则抛出异常
        if(StrUtil.isEmpty(token)){
            throw new CustomerAuthenticationException("token为空");
        }
        //03-从Redis中获取tokenKey对应的值，用于和请求中的token进行比较，redis中存储的键和值设置的都是token
        String tokenKey = "token:"+token;
        String tokenValue = redisUtils.get(tokenKey);
        //04-如果redis中不存在tokenKey对应的值，则抛出token已过期异常
        if(StrUtil.isEmpty(tokenValue)){
            throw new CustomerAuthenticationException("token已经过期");
        }
        //04-如果请求中的token和redis中的token不一致，则抛出token错误异常
        if(!StrUtil.equals(token, tokenValue)){
            throw new CustomerAuthenticationException("token错误");
        }
        //05-解析token，获取用户名
        String username = JwtUtils.parseToken(token)
                .getClaim("username").toString();
        //06-如果无法解析token，则抛出token解析失败异常
        if (StrUtil.isEmpty(username)){
           throw new CustomerAuthenticationException("token解析失败");
        }
        //07-根据username用户名加载用户详情
        UserDetails userDetails = customerUserDetailsService.loadUserByUsername(username);
        //08-如果用户不存在，则抛出异常
        if(userDetails == null){
            throw new CustomerAuthenticationException("用户不存在");
        }
        /* 09
        * 创建UsernamePasswordAuthenticationToken认证令牌，用于封装用户认证信息
        * 参数一：userDetails是包含用户详情的对象
        * 参数二：null，凭证通常是密码，因为之前认证流程中已经校验过了
        * 参数三：userDetails.getAuthorities()是用户所拥有的权限集合，用于后续的授权操作
        * */
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(userDetails, null,
                        userDetails.getAuthorities());
        /* 10
        * 设置令牌的详细信息，使用WebAuthenticationDetailsSource类来构建
        * WebAuthenticationDetailsSource会从HttpServletRequest对象中提取认证相关信息，比如IP地址，会话id等
        * request是当前的ttpServletRequest对象，包含客户端请求的信息
        * */
        authenticationToken.setDetails(new WebAuthenticationDetailsSource()
                .buildDetails(request));
        /* 11
        * 将认证令牌设置到SecurityContext中，SecurityContext是SpringSecurity中用于存储当前认证信息的上下文对象
        * SecurityContextHolder是一个工具类，用于获取和设置当前线程的SecurityContext
        * 通过调setAuthentication方法，将authenticationToken存储到SecurityContext中
        * 这样SpringSecurity就知道当前用户已经通过认证，并且可在后续的请求处理中使用这些认证信息进行授权操作
        * */
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    }
}
